Listing :

Phonebook full listing

import sys
import pickle
from functools import total_ordering
from collections import UserList


def none_or_empty(text):
    return text is None or not text.strip()


def validate_integer_range(question, start, end, default=None):
    while True:
        try:
            entry = input(question)
            if none_or_empty(entry) and default is not None:
                entry = default
            value = int(entry)
            if start <= value <= end:
                return value
        except ValueError:
            print(f"Invalid value, please enter between {start} and {end}")


def validate_integer_range_or_blank(question, start, end):
    while True:
        try:
            entry = input(question)
            if none_or_empty(entry):
                return None
            value = int(entry)
            if start <= value <= end:
                return value
        except ValueError:
            print(f"Invalid value, please enter between {start} and {end}")


class UniqueList(UserList):
    def __init__(self, elem_class, enumerable=None):
        super().__init__(enumerable)
        self.elem_class = elem_class

    def append(self, elem):
        self.verify_type(elem)
        if elem not in self.data:
            super().append(elem)

    def __setitem__(self, posição, elem):
        self.verify_type(elem)
        if elem not in self.data:
            super().__setitem__(posição, elem)

    def verify_type(self, elem):
        if not isinstance(elem, self.elem_class):
            raise TypeError("Invalid Type")

    def search(self, elem):
        self.verify_type(elem)
        try:
            return self.index(elem)
        except ValueError:
            return -1


@total_ordering
class Name:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

    def __repr__(self):
        return f"<Class {type(self).__name__} in 0x{id(self):x} Name: {self.__name} Key: {self.__key}>"

    def __eq__(self, other):
        return self.name == other.name

    def __lt__(self, other):
        return self.name < other.name

    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, value):
        if none_or_empty(value):
            raise ValueError("Name cannot be None or empty")
        self.__name = value
        self.__key = Name.CreateKey(value)

    @staticmethod
    def CreateKey(name):
        return name.strip().lower()


@total_ordering
class PhoneType:
    def __init__(self, type):
        self.type = type

    def __str__(self):
        return f"({self.type})"

    def __eq__(self, other):
        if other is None:
            return False
        return self.type == other.type

    def __lt__(self, other):
        return self.type < other.type


class Telephone:
    def __init__(self, number, type=None):
        self.number = number
        self.type = type

    def __str__(self):
        type = self.type or ""
        return f"{self.number} {type}"

    def __eq__(self, other):
        return self.number == other.number and (
            (self.type == other.type) or (self.type is None or other.type is None)
        )

    @property
    def number(self):
        return self.__number

    @number.setter
    def number(self, value):
        if none_or_empty(value):
            raise ValueError("Number cannot be None or empty")
        self.__number = value


class Telephones(UniqueList):
    def __init__(self):
        super().__init__(Telephone)


class PhoneTypes(UniqueList):
    def __init__(self):
        super().__init__(PhoneType)


class PhoneData:
    def __init__(self, name):
        self.name = name
        self.telephones = Telephones()

    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, value):
        if type(value) != Name:
            raise TypeError("name must be an instance of Name class")
        self.__name = value

    def search_phone(self, phone):
        position = self.telephones.search(Telephone(phone))
        if position == -1:
            return None
        else:
            return self.telephones[position]


class Phonebook(UniqueList):
    def __init__(self):
        super().__init__(PhoneData)
        self.telephone_types = PhoneTypes()

    def add_type(self, type):
        self.telephone_types.append(PhoneType(type))

    def search_name(self, name):
        if isinstance(name, str):
            name = Name(name)
        for data in self:
            if data.name == name:
                return data
        else:
            return None

    def sort(self):
        super().sort(key=lambda data: str(data.name))


class Menu:
    def __init__(self):
        self.options = [["Exit", None]]

    def add_option(self, name, function):
        self.options.append([name, function])

    def show(self):
        print("====")
        print("Menu")
        print("====\n")
        for i, option in enumerate(self.options):
            print(f"[{i}] - {option[0]}")
        print()

    def execute(self):
        while True:
            self.show()
            choice = validate_integer_range(
                "Choose an option: ", 0, len(self.options) - 1
            )
            if choice == 0:
                break
            self.options[choice][1]()


class PhonebookApp:
    @staticmethod
    def ask_name():
        return input("Name: ")

    @staticmethod
    def ask_telephone():
        return input("Phone: ")

    @staticmethod
    def show_data(data):
        print(f"Name: {data.name}")
        for phone in data.telephones:
            print(f"Phone: {phone}")
        print()

    @staticmethod
    def show_phone_data(data):
        print(f"Name: {data.name}")
        for i, phone in enumerate(data.telephones):
            print(f"{i} - Phone: {phone}")
        print()

    @staticmethod
    def ask_filename():
        return input("File name: ")

    def __init__(self):
        self.phonebook = Phonebook()
        self.phonebook.add_type("Cell")
        self.phonebook.add_type("Home")
        self.phonebook.add_type("Work")
        self.phonebook.add_type("Fax")
        self.menu = Menu()
        self.menu.add_option("New", self.new)
        self.menu.add_option("Edit", self.edit)
        self.menu.add_option("Delete", self.delete)
        self.menu.add_option("List", self.list)
        self.menu.add_option("Save", self.save)
        self.menu.add_option("Load", self.load)
        self.menu.add_option("Sort", self.sort)
        self.last_name = None

    def ask_telephone_type(self, default=None):
        for i, type in enumerate(self.phonebook.telephone_types):
            print(f" {i} - {type} ", end=None)
        t = validate_integer_range(
            "Type: ", 0, len(self.phonebook.telephone_types) - 1, default
        )
        return self.phonebook.telephone_types[t]

    def search(self, name):
        data = self.phonebook.search_name(name)
        return data

    def new(self):
        new = PhonebookApp.ask_name()
        if none_or_empty(new):
            return
        name = Name(new)
        if self.search(name) is not None:
            print("Name already exists!")
            return
        record = PhoneData(name)
        self.phone_menu(record)
        self.phonebook.append(record)

    def delete(self):
        if len(self.phonebook) == 0:
            print("Empty phonebook, nothing to delete")
        name = PhonebookApp.ask_name()
        if none_or_empty(name):
            return
        p = self.search(name)
        if p is not None:
            self.phonebook.remove(p)
            print(f"Deleted. The phonebook now has only: {len(self.phonebook)} records")
        else:
            print("Name not found.")

    def edit(self):
        if len(self.phonebook) == 0:
            print("Empty phonebook, nothing to edit")
        name = PhonebookApp.ask_name()
        p = self.search(name)
        if none_or_empty(name):
            return
        if p is not None:
            print("\nFound:\n")
            PhonebookApp.show_data(p)
            print("Enter to keep the same name")
            name = PhonebookApp.ask_name()
            if not none_or_empty(name):
                p.name = Name(name)
            self.phone_menu(p)
        else:
            print("Name not found!")

    def phone_menu(self, data):
        while True:
            print("\nEditing phones\n")
            PhonebookApp.show_phone_data(data)
            if len(data.telephones) > 0:
                print("\n[E] - edit\n[D] - delete\n", end="")
            print("[N] - new\n[X] - exit\n")
            operation = input("Choose an operation: ")
            operation = operation.lower()
            if operation not in ["e", "d", "n", "x"]:
                print("Invalid operation. Enter E, D, N or X")
                continue
            if operation == "e" and len(data.telephones) > 0:
                self.edit_phones(data)
            elif operation == "d" and len(data.telephones) > 0:
                self.delete_phone(data)
            elif operation == "n":
                self.new_phone(data)
            elif operation == "x":
                break

    def new_phone(self, data):
        phone = PhonebookApp.ask_telephone()
        if none_or_empty(phone):
            return
        if data.search_phone(phone) is not None:
            print("Phone already exists")
        type = self.ask_telephone_type()
        data.telephones.append(Telephone(phone, type))

    def delete_phone(self, data):
        t = validate_integer_range_or_blank(
            "Enter the position of the number to delete, enter to exit: ",
            0,
            len(data.telephones) - 1,
        )
        if t is None:
            return
        data.telephones.remove(data.telephones[t])

    def edit_phones(self, data):
        t = validate_integer_range_or_blank(
            "Enter the position of the number to edit, enter to exit: ",
            0,
            len(data.telephones) - 1,
        )
        if t is None:
            return
        phone = data.telephones[t]
        print(f"Phone: {phone}")
        print("Enter to keep the same number")
        telephone_number = PhonebookApp.ask_telephone()
        if not none_or_empty(telephone_number):
            phone.number = telephone_number
        print("Enter to keep the same type")
        phone.type = self.ask_telephone_type(
            self.phonebook.telephone_types.search(phone.type)
        )

    def list(self):
        print("\nPhonebook")
        print("-" * 60)
        for e in self.phonebook:
            PhonebookApp.show_data(e)
        print("-" * 60)

    def load(self, filename=None):
        if filename is None:
            filename = PhonebookApp.ask_filename()
        if none_or_empty(filename):
            return
        with open(filename, "rb") as file:
            self.phonebook = pickle.load(file)
        self.last_name = filename

    def sort(self):
        self.phonebook.sort()
        print("\nPhonebook sorted\n")

    def save(self):
        if self.last_name is not None:
            print(f"Last used name was '{self.last_name}'")
            print("Enter to use the same name")
        filename = PhonebookApp.ask_filename()
        if none_or_empty(filename):
            if self.last_name is not None:
                filename = self.last_name
            else:
                return
        with open(filename, "wb") as file:
            pickle.dump(self.phonebook, file)

    def execute(self):
        self.menu.execute()


if __name__ == "__main__":
    app = PhonebookApp()
    if len(sys.argv) > 1:
        app.load(sys.argv[1])
    app.execute()
Click here to download the file